﻿using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CrmClient.Mapping;
using Microsoft.CSharp;
using Microsoft.Xrm.Sdk;
using System.Reflection;

namespace CrmClient.QueryConvert
{
    public interface IItemConvertor
    {
        object CreateItem(Entity entity, object[] constants);

        object GetPropertyValue(string propertyName, object instance);
    }

    internal class FastQueryConvertor<T> : DefaultQueryConvertor<T>
    {
        /// <summary>
        /// {0} - Type name
        /// {1} - Properties assign code
        /// {2} - Get property value code
        /// </summary>
        private const string _codeTemplate = @"
using System;
using Microsoft.Xrm.Sdk;
using CrmClient.QueryConvert;
using System.Collections.Generic;
using CrmClient.Mapping;
public class ItemConvertor : IItemConvertor
{{
    public object CreateItem(Entity entity, object[] constants)
    {{
        return new {0}
        {{
{1}
        }};
    }}

    public object GetPropertyValue(string propertyName, object instance)
    {{
        switch(propertyName)
        {{
            {2}
            default:
                throw new ArgumentException(""propertyName"");
        }}
    }}
}}
";
        private static bool IsNullable(Type type)
        {
            return (type.IsGenericType && type.GetGenericTypeDefinition().Equals(typeof(Nullable<>)));
        }

        private static string GetPropertiesAssignCode(PropertiesMap propertiesMap)
        {
            var builder = new StringBuilder();
            int constIndex = 0;
            foreach (var propertyMap in propertiesMap)
            {
                string val;
                if (propertyMap.IsMappedToCrmEntity)
                {
                    var typeName = IsNullable(propertyMap.Type) ? propertyMap.Type.GetGenericArguments()[0].FullName : propertyMap.Type.FullName;

                    if (propertyMap.CrmAttr.IsAliased)
                        val = string.Format("entity.Attributes.ContainsKey(\"{1}\") ? ({0})((AliasedValue)entity.Attributes[\"{1}\"]).Value : default({0})", typeName, propertyMap.CrmAttr.ToString());
                    else
                        val = string.Format("entity.Attributes.ContainsKey(\"{1}\") ? ({0})entity.Attributes[\"{1}\"] : default({0})", typeName, propertyMap.CrmAttr.ToString());
                }
                else
                    val = string.Format("({0})constants[{1}]", propertyMap.Type.FullName, constIndex++);
                builder.AppendLine(string.Format("            {0} = {1},", propertyMap.ClassPropName, val));
            }
            return builder.ToString();
        }

        private static string GetGetPropertyValueCode(PropertiesMap propertiesMap)
        {
            var builder = new StringBuilder();
            foreach (var propertyMap in propertiesMap)
                builder.AppendLine(string.Format(@"case ""{0}"" :
    return (({1})instance).{0};", propertyMap.ClassPropName, typeof(T).FullName));
            return builder.ToString();
        }

        private static string GetCode(PropertiesMap propertiesMap)
        {
            return string.Format(_codeTemplate, typeof(T).FullName, GetPropertiesAssignCode(propertiesMap), GetGetPropertyValueCode(propertiesMap));
        }
        private static object[] GetConstants(PropertiesMap propertiesMap)
        {
            var res = new List<object>();
            foreach (var propMap in propertiesMap.Where(i => !i.IsMappedToCrmEntity))
                res.Add(propMap.ConstantValue);
            return res.ToArray();
        }

        private static Dictionary<Type, IItemConvertor> _itemConvertors = new Dictionary<Type, IItemConvertor>();

        private static List<string> _dominAssemblies;
        protected static List<string> DominAssemblies
        {
            get
            {
                if (_dominAssemblies == null)
                    _dominAssemblies = AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic).Select(a => a.Location).ToList();
                return _dominAssemblies;
            }
        }

        private static IItemConvertor GetItemConvertor(PropertiesMap propertiesMap)
        {
            lock (_itemConvertors)
            {
                IItemConvertor res;
                if (_itemConvertors.ContainsKey(typeof(T)))
                    res = _itemConvertors[typeof(T)];
                else
                {
                    var code = GetCode(propertiesMap);
                    using (var c = new CSharpCodeProvider())
                    {
                        var compilerParameters = new CompilerParameters();
                        foreach (var r in DominAssemblies)
                            compilerParameters.ReferencedAssemblies.Add(r);
                        compilerParameters.CompilerOptions = "/t:library ";
                        compilerParameters.GenerateInMemory = true;
                        var compilerResults = c.CompileAssemblyFromSource(compilerParameters, code);
                        if (compilerResults.Errors.Count > 0)
                            throw new Exception("При компиляции ItemConvertor возникли ошибки.");
                        res = Activator.CreateInstance(compilerResults.CompiledAssembly.GetExportedTypes()[0]) as IItemConvertor;
                        _itemConvertors.Add(typeof(T), res);
                    }
                }
                return res;
            }
        }

        public FastQueryConvertor(PropertiesMap propertiesMap, string entityName)
            : base(propertiesMap, entityName)
        { }

        public FastQueryConvertor()
            : base()
        { }

        protected override T CreateItemAndFill(Entity entity)
        {
            var convertor = GetItemConvertor(PropertiesMap);
            var item = convertor.CreateItem(entity, GetConstants(PropertiesMap));
            return (T)item;
        }

        protected override T CreateItemWithConstructor(Entity entity)
        {
            /* Создавать анонимные типы таким же образом что и обычные нельзя, так как они являются внутренними типами сборки, и не могут быть приведены к таким же (по составу) типам в другой сборке. */
            return base.CreateItemWithConstructor(entity);
        }

        protected override object GetValue(PropertyMap propertyMap, Entity entity)
        {
            /*Базовую реализацию переопределять не нужно, так как в ней и так не используется reflection */
            return base.GetValue(propertyMap, entity);
        }

        protected override object GetValue(PropertyMap propertyMap, T entity)
        {
            var convertor = GetItemConvertor(PropertiesMap);
            return convertor.GetPropertyValue(propertyMap.ClassPropName, entity);
        }
    }
}
